package ngat.oss.scheduling.auctions.test;

import ngat.oss.scheduling.auctions.*;
import ngat.oss.phase2.*;
import ngat.oss.accounting.*;
import ngat.oss.*;

import ngat.phase2.*;
import ngat.phase2.db.*;

import ngat.util.*;

import java.util.*;
import java.text.*;
import java.io.*;


/** A TestHarness for trying out a simple auction.*/
public class AuctionTestHarness {

    public static final int IDLE_STATE = 1;
    public static final int WAITING_AWARD_STATE = 2;
    public static final int WAITING_NOTIFICATION_STATE = 3;
   
    public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");
    public static SimpleTimeZone UTC = new SimpleTimeZone(0, "UTC");

    SimpleAuctioneer auctioneer;

    Map bidders;

    /** Create an AuctionTestHarness.*/
    public AuctionTestHarness() {
	auctioneer = new SimpleAuctioneer();
	bidders = new HashMap();
    }

    /** Run the TestHarness using supplied config.*/
    public void run(ConfigurationProperties config) throws Exception {

	// Create a list of bidders linked to existing proposals in P2DB.
	// we allocate each one funds according to its time alloc.

	File p2dir = new File(config.getProperty("db-dir"));
	P2MLPhase2Model phase2Model = new P2MLPhase2Model(p2dir);
	phase2Model.getRoot(); // can throw a wobbly.

	File accdir = new File(config.getProperty("acc-dir"));
	AmlAccountingModel accountingModel = new AmlAccountingModel(accdir);
	accountingModel.getRoot(); // can throw a wobbly.

	long now = System.currentTimeMillis();

	Iterator it = phase2Model.listProposals();
	while (it.hasNext()) {
	    String pid = (String)it.next();
	    IProposal proposal = phase2Model.getProposal(pid);
	    
	    // extract the allocated hours balance
	    AccountHolder        pam  = (AccountHolder)accountingModel.getAccountManager(pid);
	    AccountingPeriodList list = pam.listPeriods("ALLOC_HOURS");
	    AccountingPeriod  current = list.currentPeriod(now);
	    double bal = pam.getBalance("ALLOC_HOURS", current);

	    SimpleBidder bidder = new SimpleBidder(pid, bal);
	    bidders.put(pid, bidder);
	    System.err.println("Test::Adding new bidding delegate for: "+pid+" : "+bidder);
	    
	    bidder.start();

	}

	System.err.println("Test::Starting auctioneer");
	auctioneer.start();

    }

    /** Print usage.*/
    public static void usage() {
	System.err.println("Usage: java ngat.oss.scheduling.auctions.test.AuctionTestHarness --db-dir (dir> --acc-dir <dir>");
    }


    /** Configure and run TestHarness.*/
    public static void main(String args[]) {
	
	sdf.setTimeZone(UTC);

	CommandTokenizer ct = new CommandTokenizer("--");
	ct.parse(args);

	ConfigurationProperties config = ct.getMap();

	AuctionTestHarness test = new AuctionTestHarness();

	try {
	    test.run(config);
	} catch (Exception e) {
	    e.printStackTrace();
	    usage();
	    return;
	}

    } // [AuctionTestHarness]

    private class SimpleAuctioneer extends Thread implements Auctioneer {

	/** Stores bids in current auction.*/
	private Vector bids;
	
	/** Current auction.*/
	private int auctionId = 0;
	
	/** True if we are accepting bids. ### should be volatile and access synched !!!*/
	private boolean acceptBids = false;

	SimpleAuctioneer() {
	    super("Auctioneer");
	    bids = new Vector();
	}

	public void run() {	  

	    while (true) {

		
		// New Auction.
		bids.clear();

		auctionId++;

		acceptBids = true; // we are accepting bids for auctionId
	
		long now = System.currentTimeMillis();

		// Select a period to auction.
		long period = (long)(Math.random()*1800000.0+900000.0);
	    
		long deadline = now + 10000L;

		System.err.println("Test::Auctioneer::Starting auction: "+auctionId+
				   ", For slot-size: "+(period/1000)+
				   ", Deadline: "+sdf.format(new Date(deadline))+" (in "+(deadline/1000)+" secs)");
		
		CallForProposalsEvent call = new CallForProposalsEvent(now, auctionId, this, deadline, new Long(period)); 
		 
		// Notify all bidders.
		Iterator it = bidders.keySet().iterator();
		while (it.hasNext()) {
		    String pid = (String)it.next();
		    AuctionEventListener bidder = (AuctionEventListener)bidders.get(pid);

		    if (bidder == null)
			continue;
		    // Asynchronously notify bidder.
		    bidder.notifyAuctionEvent(call);

		}

		// Await bids till deadline.
		try {Thread.sleep(deadline - now);} catch (InterruptedException ix) {}
		// Not accepting bids now.
		acceptBids = false;

		// Locate highest bid - not globally optimal !
		System.err.println("Test::Auctioneer:: Checking bids");

		double best = -9999.0;
		String wid  = null;

		Iterator ib = bids.iterator();
		while (ib.hasNext()) {
		    BidEvent be = (BidEvent)ib.next();
		    Double damt = (Double)be.getBid();
		    double bid = damt.doubleValue();
		    if (bid > best) {
			best = bid;
			wid = be.getDelegateId();
		    }
		}

		AuctionEventListener winner = (AuctionEventListener)bidders.get(wid);
		now = System.currentTimeMillis();
		ContractAwardEvent award = new ContractAwardEvent(now, auctionId, new Double(best));
		
		// Let him know..asynchronously
		winner.notifyAuctionEvent(award);

		// And snooze a while as the observations get done.
		try {Thread.sleep(15000L);} catch (InterruptedException ix) {}
		
		now = System.currentTimeMillis();
		CompletionEvent done = new CompletionEvent(now, auctionId, new String("Execution completed")); 	
		winner.notifyAuctionEvent(done);

	    }

	}
	
	public void submitBid(BidEvent be) throws Exception {

	    if (be == null)
		throw new Exception("No bid!");

	    if (!acceptBids)
		throw new Exception("Bidding is over");

	    if (be.getAuctionId() != auctionId)
		throw new Exception("Not handling auction: "+be.getAuctionId());

	    // Ok its the correct auction..

	    bids.add(be);

	}


    } // [SimpleAuctioneer]

    private class SimpleBidder extends Thread implements AuctionEventListener {
	
	/** Proposal ID.*/
	private String pid;
	
	/** Allocated hours balance.*/
	private double alloc;
	
	Vector events;
	
	int state;
	
	/** Create a SimpleBidder to represent PID with allocated hours balance.*/
	SimpleBidder(String pid, double alloc) {
	    super("Bidder:"+pid);
	    this.pid   = pid;
	    this.alloc = alloc;

	    events = new Vector();
	    state = IDLE_STATE;
	}
	
	public void run() {

	    long deadline = 0L;

	    while (true) {
		
		try {Thread.sleep(2000L);} catch (InterruptedException ix) {}

		long time = System.currentTimeMillis();

		AuctionEvent ae = null;

		// pull an event and decide what to do..needs state model
		if (events.size() != 0) 	 
		    ae = (AuctionEvent)events.remove(0);
	    		
		switch (state) {
		case IDLE_STATE:
		    if (ae != null  && ae instanceof CallForProposalsEvent) {
			CallForProposalsEvent call = (CallForProposalsEvent)ae;
			Auctioneer auctioneer = call.getAuctioneer();			
			deadline = call.getDeadline();
			Object terms = call.getTerms();
			
			if (time > deadline) 
			    System.err.println(this.toString()+":Received CFP after deadline");
			else {
			    // Simple and totally crap way to decide bid.
			    // This is where the clever stuff should happen
			    // i.e.calculation of expected utility and optimization
			    
			    double bid = alloc/100.0+ Math.random()*30.0;
			    BidEvent be = new BidEvent(time, call.getAuctionId(), pid, new Double(bid));
			    try {
				System.err.println(this.toString()+":Submitting: "+bid);
				auctioneer.submitBid(be);
				state = WAITING_AWARD_STATE;
			    } catch (Exception e) {
				System.err.println(this.toString()+":Exception while submitting bid: "+e);
				e.printStackTrace();
			    }
			}
		    }
		    break;
		case WAITING_AWARD_STATE:
		    if (ae != null  && ae instanceof ContractAwardEvent) {
			ContractAwardEvent award = (ContractAwardEvent)ae;
			Double dval  = (Double)award.getValue();
			System.err.println(this.toString()+":Awarded contract with value: "+dval.doubleValue());
			state = WAITING_NOTIFICATION_STATE;
		    } else if
			(ae != null  && ae instanceof RejectEvent) {
			RejectEvent reject = (RejectEvent)ae;
			System.err.println(this.toString()+":Rejected: "+reject.getReason());
			state = IDLE_STATE;
		    } else {
			if (time > deadline + 5000L) {
			    System.err.println(this.toString()+":No award and well after deadline, assume rejected");
			    state = IDLE_STATE;
			}
		    }
		    break;
		case WAITING_NOTIFICATION_STATE:
		    if (ae != null  && ae instanceof CompletionEvent) {
			CompletionEvent done = (CompletionEvent)ae;
			System.err.println(this.toString()+":Completed: "+done);
			state = IDLE_STATE;
		    }		    
		    break;
		default:
		    System.err.println(this.toString()+":Illegal state");
		}
		
	    }
	}
    

	public void notifyAuctionEvent(AuctionEvent ae) {
	    events.add(ae);
	}
	
	public String toStateString() {
	    switch (state) {
	    case IDLE_STATE:
		return "IDLE";
	    case WAITING_AWARD_STATE:
		return "WAITING_AWARD";
	    case WAITING_NOTIFICATION_STATE:
		return "WAITING_NOTIFY";
	    default:
		return "UNKNOWN";
	    }
	}

	public String toString() {
	    return "SBA: "+pid+", Alloc: "+alloc+", State: "+toStateString();
	}

    } // [SimpleBidder]

}
